package com.xyzwps.mvckotlin.handler

import com.xyzwps.mvckotlin.exception.BizException
import com.xyzwps.mvckotlin.exception.badRequest
import com.xyzwps.mvckotlin.exception.invalidReqArgs
import com.xyzwps.mvckotlin.service.InvitationCodeService
import com.xyzwps.mvckotlin.service.UserService
import org.apache.commons.lang3.StringUtils.isEmpty
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import org.springframework.util.MultiValueMap
import java.util.regex.Pattern
import com.xyzwps.mvckotlin.exception.userExists
import com.xyzwps.mvckotlin.handler.helper.ModelAndView
import com.xyzwps.mvckotlin.handler.helper.clearUserInfo
import com.xyzwps.mvckotlin.handler.helper.putUserInfo
import com.xyzwps.mvckotlin.handler.helper.render
import kotlinx.coroutines.reactive.awaitSingle
import java.net.URLEncoder

@Component
class EntryHandler(val userService: UserService,
                   val invitationCodeService: InvitationCodeService) {

    companion object {
        private val log = LoggerFactory.getLogger(EntryHandler::class.java)

        private const val USERNAME_MIN_SIZE = 3
        private const val USERNAME_MAX_SIZE = 24

        private const val PASSWORD_MIN_SIZE = 3
        private const val PASSWORD_MAX_SIZE = 24
    }

    fun registerPage() = render {
        val hasMoreInvitationCodes = invitationCodeService.hasMoreUnusedInvitationCodes()
        if (hasMoreInvitationCodes) {
            val optionalErrorMsg = it.queryParam("errorMsg")
            val errorMsg = if (optionalErrorMsg.isPresent) optionalErrorMsg.get() else null
            ModelAndView("register", mapOf("errorMsg" to errorMsg))
        } else {
            ModelAndView("redirect:/home")
        }
    }

    fun register() = render { req ->
        try {
            val dto = RegisterUserDto.fromFormData(req.formData().awaitSingle())
            checkUsername(dto.username)
            checkPassword(dto.password)

            if (dto.password != dto.repassword) throw invalidReqArgs("两次输入密码不一致")
            if (isEmpty(dto.invitationCode)) throw invalidReqArgs("缺少邀请码")

            val userExists = userService.checkUsernameExists(dto.username!!)
            if (userExists) throw userExists(dto.username)

            val invitationCodeExists = invitationCodeService.checkCodeExists(dto.invitationCode!!)
            log.debug("invitation code {} exists: {}", dto.invitationCode, invitationCodeExists)
            if (invitationCodeExists) invitationCodeService.markAsUsed(dto.invitationCode)
            else throw invalidReqArgs("邀请码不正确")

            userService.create(dto.username, dto.password!!)

            ModelAndView("register-succeed")
        } catch (ex: BizException) {
            ModelAndView("redirect:/user/register?errorMsg=" + URLEncoder.encode(ex.message, Charsets.UTF_8.name()))
        }
    }

    fun loginPage() = render {
        val optionalErrorMsg = it.queryParam("errorMsg")
        val errorMsg = if (optionalErrorMsg.isPresent) optionalErrorMsg.get() else null
        ModelAndView("login", mapOf("errorMsg" to errorMsg))
    }


    fun login() = render { req ->
        try {
            val dto = LoginUserDto.fromFormData(req.formData().awaitSingle())
            checkUsername(dto.username)
            checkPassword(dto.password)

            val userInfo = userService.getUserByUsername(dto.username!!)
            userInfo ?: throw badRequest("用户名或密码不正确")

            val checkResult = userService.checkPassword(dto.password!!, userInfo.password!!)

            if (checkResult) {
                val session = req.session().awaitSingle()
                session.putUserInfo(userInfo.desensitize())
                ModelAndView("redirect:/user/space")
            } else throw badRequest("用户名或密码不正确")
        } catch (ex: BizException) {
            ModelAndView("redirect:/user/login?errorMsg=" + URLEncoder.encode(ex.message, Charsets.UTF_8.name()))
        }
    }

    fun logoutPage() = render { ModelAndView("logout") }

    fun logout() = render {
        val formData = it.formData().awaitSingle()
        val logout = formData.getFirst("logout")?.toBoolean()
        if (logout != null && logout) {
            val session = it.session().awaitSingle()
            session.clearUserInfo()
        }
        ModelAndView("redirect:/home")
    }

    private fun checkUsername(username: String?) {
        when {
            username == null ->
                throw invalidReqArgs("缺少用户名")
            username.length < USERNAME_MIN_SIZE ->
                throw invalidReqArgs("用户名至少应该包含" + USERNAME_MIN_SIZE + "个字符")
            username.length > USERNAME_MAX_SIZE ->
                throw invalidReqArgs("用户名最多不应超过" + USERNAME_MAX_SIZE + "个字符")
            !Pattern.matches("^[A-Za-z0-9]{3,24}$", username) ->
                throw invalidReqArgs("用户名只能由大小写字母和数字组成")
        }
    }

    private fun checkPassword(password: String?) {
        when {
            password == null ->
                throw invalidReqArgs("密码必不可少")
            password.length < PASSWORD_MIN_SIZE ->
                throw invalidReqArgs("密码至少应该包含" + PASSWORD_MIN_SIZE + "个字符")
            password.length > PASSWORD_MAX_SIZE ->
                throw invalidReqArgs("密码最多不应超过" + PASSWORD_MAX_SIZE + "个字符")
            !Pattern.matches("^[A-Za-z0-9.-~!@#$%*]{3,24}$", password) ->
                throw invalidReqArgs("密码只能由大小写字母、数字和 .-~!@#$%* 组成")
        }
    }

}

internal data class RegisterUserDto(val username: String? = null,
                                    val password: String? = null,
                                    val repassword: String? = null,
                                    val invitationCode: String? = null) {
    companion object {
        fun fromFormData(form: MultiValueMap<String, String>) =
                RegisterUserDto(username = form.getFirst("username"),
                        password = form.getFirst("password"),
                        repassword = form.getFirst("repassword"),
                        invitationCode = form.getFirst("invitationCode"))
    }
}

internal data class LoginUserDto(val username: String? = null,
                                 val password: String? = null) {
    companion object {
        fun fromFormData(form: MultiValueMap<String, String>) =
                LoginUserDto(username = form.getFirst("username"),
                        password = form.getFirst("password"))
    }
}

